#region (C) Koninklijke Philips Electronics N.V. 2012-2013
//
// All rights are reserved. Reproduction or transmission in whole or in part, in
// any form or by any means, electronic, mechanical or otherwise, is prohibited 
// without the prior written consent of the copyright owner.
// 
// Filename: AssemblyResolver.cs
//
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Xml;
using System.Diagnostics;

#if BUILD_TESTFRAMEWORK
namespace Philips.Platform.TestFramework {
#else
namespace Philips.Platform.Tests {
#endif

    /// <summary>
    /// AssemblyResolver will resolve additional assemblies needed for testing.
    /// The location of additional assemblies are specified in the 
    /// "Philips.Platform.Tests.config" file.
    /// </summary>
    /// <remarks>
    /// <para>This code is published to Output\Shared so that test frameworks of other codebases 
    /// can include it. This code is not actually part of this test framework assembly. That is 
    /// first of all, because it is not needed here as the TestFramework sub-codebase does not have 
    /// runtime dependencies on other codebases. Secondly, although other codebases do need this
    /// runtime assembly resolution logic, they cannot use it by referencing an assembly elsewhere,
    /// as they will not be able to resolve the latter.</para>
    /// 
    /// <para>This code will be duplicated across assemblies for multiple test frameworks. The 
    /// class has been made internal to avoid ambiguity, as test code may reference multiple test
    /// framework assemblies. Each test framework should only make its internals visible to test
    /// code in its own (sub-)codebase.</para>
    /// </remarks>
    internal static class AssemblyResolver {
        //Constants
        private static List<string> AssemblyExtensions {
            get;
            set;
        }
        private const string xpath = "/configuration/paths/path";
           
        /// <summary>
        /// Gets or sets a value containing the name of the configuration file containing 
        /// paths for assembly resolution
        /// </summary>
        public static string ConfigFileName {get; set;}

        public static List<string> SearchPaths {
            get;
            set;
        }

        // Fallback assembly resolving config file name; needed in current transition phase 
        // to final dll packaging solution
        // Todo: remove this fallback file definition when final DLL packaging solution is in place
        private const string configFileNameFallback = @".\Philips.Platform.Tests.config";

        /// <summary>
        /// Double start guard.
        /// </summary>
        private static bool started;

        /// <summary>
        /// Static constructor.
        /// </summary>
        static AssemblyResolver() {
            AssemblyExtensions = new List<string> { ".dll", ".exe" };
            ConfigFileName = @"..\Test\Philips.Platform.Tests.config";
            SearchPaths = new List<string>();
        }

        /// <summary>
        /// Starts resolving assemblies.
        /// </summary>
        public static void Start() {
            lock (typeof(AssemblyResolver)) {
                if (!started) {
                    started = true;
                    ReadConfig();
                    var appDomain = AppDomain.CurrentDomain;
                    appDomain.AssemblyResolve += OnAssemblyResolve;
                    appDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve;
                }
            }
        }

        /// <summary>
        /// Stops resolving Assemblies.
        /// </summary>
        public static void Stop() {
            var appDomain = AppDomain.CurrentDomain;
            appDomain.AssemblyResolve -= OnAssemblyResolve;
            appDomain.ReflectionOnlyAssemblyResolve -= OnReflectionOnlyAssemblyResolve;
        }

        /// <summary>
        /// Add paths to the resolver.
        /// </summary>
        /// <param name="paths">Paths to add.</param>
        public static void AddPaths(IList<string> paths) {

            // fixup the relative paths to nice absolute paths
            string testAssemblyLocation =
                Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location);
            string initialPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;

            // 260 is the max length of a regular WIn32 path name ...
            StringBuilder envPath = 
                new StringBuilder(initialPath.Length + 2 * paths.Count * 260);
            envPath.Append(initialPath);
            string bitness = Environment.Is64BitProcess ? "X64" : "X86";

            for (int i = 0; i < paths.Count; i++) {
                string path = paths[i];
                if (!Path.IsPathRooted(path)) {
                    path = Path.Combine(testAssemblyLocation, path);
                    path = Path.GetFullPath(path);
                }
                if (!Directory.Exists(path)) {
                    // do not add not existing paths
                    continue;
                }
                if (!SearchPaths.Contains(path)) {
                    SearchPaths.Add(path);
                    // other assembly resolvers could add same paths to PATH
                    if (!initialPath.Contains(path)) {
                        envPath.Append(';');
                        envPath.Append(path);
                    }
                    path = Path.Combine(path, bitness);
                    if (Directory.Exists(path)) {
                        // Needed for C++ .dlls compiled with /clr flag.
                        SearchPaths.Add(path);
                        // other assembly resolvers could add same paths to PATH
                        if (!initialPath.Contains(path)) {
                            envPath.Append(';');
                            envPath.Append(path);
                        }
                    }
                }
            }
            SafeNativeMethods.SetEnvironmentVariable("PATH", envPath.ToString());
        }

        /// <summary>
        /// Read configured assembly paths from configuration file.
        /// </summary>
        private static void ReadConfig() {

            SearchPaths.Clear();
            
            // Get correct assembly resolving config file name; needed in current transition 
            // phase to final dll packaging solution
            // TODO: remove this config file choice when final DLL packaging solution is in place
            string fileName;
            string dir = Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location);

            
            if (!Path.IsPathRooted(ConfigFileName)) {
                if (dir != null) {
                    fileName = Path.Combine(dir, ConfigFileName);
                    fileName = Path.GetFullPath(fileName);
                    if (File.Exists(fileName)) {
                        ConfigFileName = fileName;
                    }
                }
            } 
            
            if (File.Exists(ConfigFileName)) {
                fileName = ConfigFileName;
            } else if (File.Exists(configFileNameFallback)) {
                fileName = configFileNameFallback;
            } else {
                // No file found, no issue when running from a single directory
                // the configuration is  only really required in developer setup.
                Type type = typeof(AssemblyResolver);
                Assembly assembly = type.Assembly;
                Console.WriteLine("AssemblyResolver hosted in: " + assembly.GetName().Name);
                Console.WriteLine("Search location: " + dir);
                Console.WriteLine("Unable to locate assembly resolving configuration file!");
                Console.WriteLine(" Filenames: ");
                Console.WriteLine(" - " + ConfigFileName);
                Console.WriteLine(" - " + configFileNameFallback);
                fileName = null;
                
            }

            List<string> paths = new List<string>();
            if (fileName == null) {
                // best guess .. add current assemblies location
                paths.Add(dir);
            } else {
                // Read xml file:
                StreamReader sr = new StreamReader(fileName);
                string xmlString = sr.ReadToEnd();
                // Parse xml file:
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(xmlString);
                XmlNodeList pathNodes = doc.SelectNodes(xpath);
                if (pathNodes != null) {
                    foreach (XmlNode path in pathNodes) {
                        paths.Add(path.InnerText);
                    }
                }
            }
            AddPaths(paths);
        }

        /// <summary>
        /// Attempts to <see cref="Assembly.Load(string)"/>load an assembly that could not be 
        /// resolved.
        /// </summary>
        private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) {
            string path = null;
            try {
                path = FindAssembly(args.Name);
                return path != null ? Assembly.LoadFrom(path) : null;
            } catch(Exception e) {
                Console.WriteLine("Assembly load failed");
                Console.WriteLine(e.Message);
                Console.WriteLine(path ?? "<empty>");
                throw;
            }
        }

        /// <summary>
        /// Attempts to <see cref="Assembly.ReflectionOnlyLoad(string)"/> an assembly that 
        /// could not be resolved.
        /// </summary>
        private static Assembly OnReflectionOnlyAssemblyResolve(
            object sender,
            ResolveEventArgs args
        ) {
            string path = FindAssembly(args.Name);
            return path != null ? Assembly.ReflectionOnlyLoadFrom(path) : null;
        }

        /// <summary>
        /// Checks if an assembly is available on the predefined search paths and returns that
        /// path.
        /// </summary>
        private static string FindAssembly(string assemblyFullName) {
            string[] assemblyName = assemblyFullName.Split(',');
            string assemblyFilename = assemblyName[0];
            string filename = null;
            foreach (string dir in SearchPaths) {
                foreach (var extension in AssemblyExtensions) {
                    string combinedPath = Path.Combine(dir, assemblyFilename) + extension;
                    if (File.Exists(combinedPath) && CanLoad(assemblyFullName, combinedPath)) {
                        filename = combinedPath;
                        break;
                    }
                }
            }
            return filename;
        }

        /// <summary>
        /// Checks if the assembly File can be loaded or not
        /// </summary>
        private static bool CanLoad(string requestedAssembly, string assemblyFile) {
            bool canLoad = true;

            AssemblyName requestedAssemblyName = new AssemblyName(requestedAssembly);
            string requestedAssemblyPublickey = GetPublicKeyToken(requestedAssembly);

            AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyFile);
            string assemblyPublicKey = GetPublicKeyToken(assemblyName.FullName);
            ProcessorArchitecture assemblyBitness = assemblyName.ProcessorArchitecture;
            // check version 
            bool versionMatches =
                assemblyName.Version.Equals(requestedAssemblyName.Version) ||
                requestedAssemblyName.Version == null;
            bool publicKeyMatches = assemblyPublicKey.Equals(
                requestedAssemblyPublickey,
                StringComparison.OrdinalIgnoreCase
            ) || (requestedAssemblyPublickey == null);

            bool procArchitectureMatches = ProcessorArchitectureMatches(assemblyBitness);
            canLoad = versionMatches && publicKeyMatches && procArchitectureMatches;
            if (!canLoad) {
                string msg = "Cannot load assembly " + assemblyFile + ". Reason: ";
                if(!versionMatches){
                    msg += "Invalid version, requested:" + requestedAssemblyName.Version +
                    ", actual:" + assemblyName.Version +". ";
                }
                if(!publicKeyMatches){
                    msg += "Invalid publicKey, requested:" + requestedAssemblyPublickey +
                    ", actual:" + assemblyPublicKey +". ";
                }
                if(!procArchitectureMatches){                    
                    msg += "Invalid ProcArchitecture.";
                }
                Trace.WriteLine(msg);
            }
            return canLoad;
        }

        /// <summary>
        /// Helper method to check for ProcessArchitecture
        /// </summary>
        private static bool ProcessorArchitectureMatches(
            ProcessorArchitecture assemblyBitness) {
            bool matches = assemblyBitness == ProcessorArchitecture.MSIL;
            if (!matches) {
                if (Environment.Is64BitProcess) {
                    matches = assemblyBitness == ProcessorArchitecture.Amd64 ||
                        assemblyBitness == ProcessorArchitecture.IA64;
                } else {
                    matches = assemblyBitness == ProcessorArchitecture.X86;
                }
            }
            return matches;
        }

        /// <summary>
        /// The PublicKeyToken in the assembly name, or null if not present.
        /// </summary>
        public static string GetPublicKeyToken(string assemblyName) {
            return GetAttributeValue(assemblyName, "PublicKeyToken");
        }

        /// <summary>
        /// Gets the value of one of the attributes from the attribute list
        /// </summary>
        /// <param name="attributeList">The string containing comma separated list of 
        /// attributes</param>
        /// <param name="term">The attribute to look for</param>
        /// <returns>the value of the term, or null if the term is not present</returns>
        private static string GetAttributeValue(string attributeList, string term){
            if (attributeList == null) {
                return null;
            }
            int termIndex = attributeList.IndexOf(term, 0, StringComparison.Ordinal);
            if (termIndex<0) {
                return null;
            } else {
                int valueIndex = termIndex + term.Length + 1; // +1 for '=' sign
                int commaIndex = attributeList.IndexOf(",", valueIndex, StringComparison.Ordinal);
                if (commaIndex>0) {
                    return attributeList.Substring(valueIndex, commaIndex - valueIndex);
                } else {
                    return attributeList.Substring(valueIndex);
                }
            }
        }
    }

    /// <summary>
    /// Contains Native methods which are safe to be called from managed code
    /// </summary>
    internal static class SafeNativeMethods{

        /// <summary>
        /// The SetEnvironmentVariable function sets the contents of 
        /// the specified environment variable for the current process.
        /// </summary>
        /// <remarks>
        /// NOTE: Environment.SetEnvironmentVariable is NOT reliable
        /// Using that one will not make the data available to next call to 
        /// Environment.GetEnvironmentVariable.
        /// </remarks>
        /// <param name="lpName">
        /// Pointer to a null-terminated string that specifies the 
        /// name of the environment variable. 
        /// The operating system creates the environment variable 
        /// if it does not exist and lpValue is not NULL. 
        /// </param>
        /// <param name="lpValue">
        /// Pointer to a null-terminated string that specifies the 
        /// contents of the environment variable. 
        /// An environment variable has a maximum size limit of 32,767 bytes, 
        /// including the trailing null terminator. 
        /// If this parameter is NULL, 
        /// the variable is deleted from the current process's environment. 
        /// </param>
        /// <returns>
        /// If the function succeeds, the return value is nonzero. 
        /// If the function fails, the return value is zero. 
        /// To get extended error information, call GetLastError.
        /// </returns>
        [SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetEnvironmentVariable(string lpName, string lpValue);
    }
}
